Coverage Report

Created: 2026-08-07 16:19

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
D:\a\cssh-rs\cssh-rs\xtask\src\record_demo.rs
Line
Count
Source
1
//! Record the cssh-rs demo GIF via the `demo/` package.
2
//!
3
//! The Rust side is a thin shell: it builds the binary and installs the
4
//! packages, then the `demo/suites/record_demo.robot` suite drives real
5
//! console windows and records the desktop. Windows only.
6
7
use std::path::{Path, PathBuf};
8
9
use anyhow::{bail, Context, Result};
10
11
/// All side-effecting operations required by this module.
12
///
13
/// Implement with mocks in tests to achieve zero process and filesystem
14
/// side-effects.
15
pub trait DemoSystem {
16
    /// Return `true` when running on Windows, the demo's only supported host.
17
    fn is_windows(&self) -> bool;
18
19
    /// Return the absolute path to the workspace root (parent of `xtask/`).
20
    ///
21
    /// # Errors
22
    /// Returns an error if the workspace root cannot be resolved.
23
    fn workspace_root(&self) -> Result<PathBuf>;
24
25
    /// Build the debug cssh-rs binary and return its path.
26
    ///
27
    /// # Errors
28
    /// Returns an error if the build fails.
29
    fn build_binary(&self) -> Result<PathBuf>;
30
31
    /// Install a Python package directory in editable mode.
32
    ///
33
    /// # Arguments
34
    /// * `package_dir` - Directory holding the package's `pyproject.toml`.
35
    ///
36
    /// # Errors
37
    /// Returns an error if the install fails.
38
    fn pip_install(&self, package_dir: &Path) -> Result<()>;
39
40
    /// Run the demo recorder robot suite to produce the GIF.
41
    ///
42
    /// # Arguments
43
    /// * `binary` - Path to the cssh-rs executable to drive.
44
    /// * `output_dir` - Directory for the intermediate MP4.
45
    /// * `gif` - Destination path for the exported GIF.
46
    ///
47
    /// # Errors
48
    /// Returns an error if the recorder exits with a non-zero status.
49
    fn run_demo(&self, binary: &Path, output_dir: &Path, gif: &Path) -> Result<()>;
50
51
    /// Return `true` when `path` exists.
52
    ///
53
    /// # Arguments
54
    /// * `path` - Filesystem path to probe.
55
    fn path_exists(&self, path: &Path) -> bool;
56
}
57
58
/// Production implementation of [`DemoSystem`].
59
pub struct RealSystem;
60
61
#[cfg_attr(coverage_nightly, coverage(off))]
62
impl DemoSystem for RealSystem {
63
    fn is_windows(&self) -> bool {
64
        cfg!(windows)
65
    }
66
67
    fn workspace_root(&self) -> Result<PathBuf> {
68
        // CARGO_MANIFEST_DIR points at xtask/, whose parent is the workspace root.
69
        Ok(Path::new(env!("CARGO_MANIFEST_DIR"))
70
            .parent()
71
            .context("failed to resolve workspace root from CARGO_MANIFEST_DIR")?
72
            .to_path_buf())
73
    }
74
75
    fn build_binary(&self) -> Result<PathBuf> {
76
        let root = self.workspace_root()?;
77
        let status = std::process::Command::new("cargo")
78
            .args(["build", "-p", "cssh-rs"])
79
            .status()
80
            .context("failed to spawn `cargo build -p cssh-rs`")?;
81
        if !status.success() {
82
            bail!("`cargo build -p cssh-rs` failed with status {status}");
83
        }
84
        Ok(root.join("target").join("debug").join("cssh-rs.exe"))
85
    }
86
87
    fn pip_install(&self, package_dir: &Path) -> Result<()> {
88
        let status = std::process::Command::new("python")
89
            .args(["-m", "pip", "install", "-e"])
90
            .arg(package_dir)
91
            .status()
92
            .context("failed to spawn `python -m pip install`; is Python on PATH?")?;
93
        if !status.success() {
94
            bail!(
95
                "`pip install -e {}` failed with status {status}",
96
                package_dir.display()
97
            );
98
        }
99
        Ok(())
100
    }
101
102
    fn run_demo(&self, binary: &Path, output_dir: &Path, gif: &Path) -> Result<()> {
103
        let suite = self
104
            .workspace_root()?
105
            .join("demo")
106
            .join("suites")
107
            .join("record_demo.robot");
108
        // `python -m robot` uses the interpreter that installed the packages; a
109
        // bare `robot` needs the Scripts dir on PATH, which is not guaranteed.
110
        // `--variable NAME:VALUE` splits on the first colon, so `C:\...` survives.
111
        let status = std::process::Command::new("python")
112
            .args(["-m", "robot"])
113
            .arg("--variable")
114
            .arg(format!("BINARY:{}", binary.display()))
115
            .arg("--variable")
116
            .arg(format!("OUTPUT_DIR:{}", output_dir.display()))
117
            .arg("--variable")
118
            .arg(format!("GIF:{}", gif.display()))
119
            .arg("--outputdir")
120
            .arg(output_dir)
121
            .arg(&suite)
122
            .status()
123
            .context("failed to spawn `python -m robot`; is Python on PATH and the demo package installed?")?;
124
        if !status.success() {
125
            bail!("demo recorder (robot) exited with status {status}");
126
        }
127
        Ok(())
128
    }
129
130
    fn path_exists(&self, path: &Path) -> bool {
131
        path.exists()
132
    }
133
}
134
135
/// Record the demo GIF into `target/demo/cssh-rs.gif`.
136
///
137
/// The GIF lands under `target/` so Cargo's `.gitignore` covers it;
138
/// publishing it is a separate step.
139
///
140
/// # Arguments
141
/// * `system` - Injected I/O provider.
142
///
143
/// # Errors
144
/// Returns an error on a non-Windows host, or if any build, install or record
145
/// step fails, or if no GIF is produced.
146
3
pub fn record_demo<S: DemoSystem>(system: &S) -> Result<()> {
147
3
    if !system.is_windows() {
148
1
        bail!("`record-demo` runs on Windows only: it drives real console windows and records the desktop");
149
2
    }
150
2
    let root = system.workspace_root()
?0
;
151
152
2
    log::info!("Building cssh-rs (debug) for the demo");
153
2
    let binary = system.build_binary()
?0
;
154
155
4
    for package in 
["automation", "demo"]2
{
156
4
        let dir = root.join(package);
157
4
        log::info!("Installing {} (editable)", dir.display());
158
4
        system.pip_install(&dir)
?0
;
159
    }
160
161
2
    let output_dir = root.join("target").join("demo");
162
2
    let gif = output_dir.join("cssh-rs.gif");
163
2
    log::info!("Recording demo -> {}", gif.display());
164
2
    system.run_demo(&binary, &output_dir, &gif)
?0
;
165
166
2
    if !system.path_exists(&gif) {
167
1
        bail!(
168
            "demo recorder finished but no GIF was written at {}",
169
1
            gif.display()
170
        );
171
1
    }
172
1
    log::info!("Wrote demo GIF -> {}", gif.display());
173
1
    Ok(())
174
3
}
175
176
#[cfg(test)]
177
#[path = "tests/test_record_demo.rs"]
178
mod tests;